﻿//===============================================================================
// CB Stock Data Library
//===============================================================================
// Copyright © CB.  All rights reserved.
// THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, 
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS 
// OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 
// WITH THE CODE OR THE USE OR OTHER DEALINGS IN THE CODE.
//===============================================================================

using System;
using System.Collections.Generic;
using System.Collections.Concurrent;
using System.IO;
using System.Linq;
using System.Net;
using System.Reflection;
using System.Text;
using System.Xml.Linq;
using CB.Stock.Core;
using Microsoft.Practices.Unity;

namespace CB.Stock.Data.Yahoo
{
    public class YahooStockDataService : IStockDataService
    {
        private const string NameSpace = "CB.Stock.Data.Yahoo.";

        #region Helper Class
        private enum TimeType
        {
            Timestamp,
            Date
        }

        private class TimestampTimeParser : IValueParser
        {
            #region Implementation of IValueParser<out DateTime>

            public object Parse(string value)
            {
                const int timeBase = 1289957400;
                var currentTimeNumber = int.Parse(value);
                return new DateTime(2010, 11, 17, 1, 30, 0, DateTimeKind.Utc).AddSeconds(currentTimeNumber - timeBase).ToLocalTime();
            }

            #endregion
        }

        private class DateTimeParser : IValueParser
        {
            #region Implementation of IValueParser<out DateTime>

            public object Parse(string value)
            {
                return new DateTime(int.Parse(value.Substring(0, 4)), int.Parse(value.Substring(4, 2)),
                                    int.Parse(value.Substring(6)));
            }

            #endregion
        }
        #endregion
        /// <summary>
        /// store the query parameters for each StockDataType
        /// </summary>
        private static readonly ConcurrentDictionary<StockDataType, string> CurrentDataParamsDict;

        static YahooStockDataService()
        {
            CurrentDataParamsDict = new ConcurrentDictionary<StockDataType, string>();
            CurrentDataParamsDict[StockDataType.SalePrice] = "p5";
            CurrentDataParamsDict[StockDataType.PaidPrice] = "p1";
            CurrentDataParamsDict[StockDataType.Symbol] = "s";
            CurrentDataParamsDict[StockDataType.LastTradeDate] = "d1";
            CurrentDataParamsDict[StockDataType.LastTradePrice] = "l1";
            CurrentDataParamsDict[StockDataType.Open] = "o";
            CurrentDataParamsDict[StockDataType.Volume] = "v";
        }

        private readonly IUnityContainer _container;
        public YahooStockDataService(IUnityContainer container)
        {
            if (container == null)
            {
                container = new UnityContainer();
            }
            _container = container;
            RegisterTypes(container);
        }

        protected void RegisterTypes(IUnityContainer container)
        {
            #region Registe StockDataType
            var baseParserType = typeof (YahooValueParser<>);
            foreach (var t in typeof(StockDataType).GetFields(BindingFlags.Static | BindingFlags.GetField | BindingFlags.Public))
            {
                var a =
                    (StockDataValueTypeAttribute)t.GetCustomAttributes(typeof(StockDataValueTypeAttribute), false)[0];
                container.RegisterType(typeof (IStockDynamicData),
                                       DefaultDynamicData<object>.GetDefaultDynamicDataType((StockDataType) t.GetValue(null)),
                                       NameSpace + t.GetValue(null));
                if (a.ValueType == typeof(int))
                {
                    container.RegisterType<IValueParser, YahooValueParserInt>(NameSpace + t.GetValue(null));
                }
                else
                {
                    container.RegisterType(typeof (IValueParser), baseParserType.MakeGenericType(a.ValueType),
                                           NameSpace + t.GetValue(null));
                }
            }
            #endregion

            #region Registe Time Parser

            container.RegisterType<IValueParser, DateTimeParser>(NameSpace + "TimeType." + TimeType.Date);
            container.RegisterType<IValueParser, TimestampTimeParser>(NameSpace + "TimeType." + TimeType.Timestamp);

            #endregion
        }

        private IValueParser ResolveValueParser(StockDataType dataType)
        {
            return _container.Resolve<IValueParser>(NameSpace + dataType);
        }

        private IValueParser ResolveValueParser(TimeType type)
        {
            return _container.Resolve<IValueParser>(NameSpace + "TimeType." + type);
        }

        private IStockDynamicData ResolveStockDynamicData(StockDataType dataType, params ParameterOverride[] parameters)
        {
            return _container.Resolve<IStockDynamicData>(NameSpace + dataType, parameters);
        }

        private static string GetCurrentDataQueryParam(StockDataType[] dataTypes)
        {
            var result = new StringBuilder();
            foreach (var dt in dataTypes)
            {
               result.Append(CurrentDataParamsDict[dt]);
            }
            return result.ToString();
        }

        private static string GetStockQueryId(IStockStaticInformation stock)
        {
            string marketTag;
            switch (stock.Market)
            {
                case MarketType.ShangHai:
                    marketTag = "SS";
                    break;
                case MarketType.ShenZhen:
                    marketTag = "SZ";
                    break;
                default:
                    throw new ArgumentOutOfRangeException();
            }
            return string.Format("{0}.{1}", stock.Code, marketTag).ToUpper();
        }

        /// <summary>
        /// get all the stocks current data
        /// </summary>
        /// <param name="dataTypes"></param>
        /// <param name="stockInfors">all the stock information</param>
        /// <returns>Key is the data type and value is all the data information of same type of each sotck </returns>
        public IEnumerable<Tuple<IStockStaticInformation, IDictionary<StockDataType, IStockDynamicData>>> GetCurrentData(StockDataType[] dataTypes, params IStockStaticInformation[] stockInfors)
        {
            var stockInformations = new Dictionary<string, IStockStaticInformation>(); //key is the stock querty id
            var param = new StringBuilder("http://finance.yahoo.com/d/quotes.csv?s=");

            //get query ids
            for (int i = 0; i < stockInfors.Count(); i++)
            {
                var id = GetStockQueryId(stockInfors[i]);
                stockInformations[id] = stockInfors[i];

                if (i > 0)
                {
                    param.Append('+');
                }
                param.Append(id);
            }

            //get the query parameters
            param.Append("&f=");
            //always get the stock query id, so that we can check the secquence order of the result stocks 
            param.Append(GetCurrentDataQueryParam(new[] {StockDataType.Symbol}));
            if (dataTypes == null || dataTypes.Length <= 0)
            {
                dataTypes = (from t in CurrentDataParamsDict.Keys where t != StockDataType.Symbol select t).ToArray();
            }
            param.Append(GetCurrentDataQueryParam(dataTypes));

            //get result data
            var request = WebRequest.Create(param.ToString());
            var response = request.GetResponse();
            try
            {
                using (var stream = response.GetResponseStream())
                {
                    var time = DateTime.Now;
                    using (var sr = new StreamReader(stream))
                    {
                        var line = sr.ReadLine();
                        while (!string.IsNullOrEmpty(line))
                        {
                            var elements = line.Split(',');

                            var result = new Dictionary<StockDataType, IStockDynamicData>();
                            //as s is always the firt query parameter
                            for (int i = 0; i < dataTypes.Length; i++)
                            {
                                IStockDynamicData data = null;
                                try
                                {
                                    data = ResolveStockDynamicData(dataTypes[i],
                                        new ParameterOverride("dataType", dataTypes[i]),
                                        new ParameterOverride("value",ResolveValueParser(dataTypes[i]).Parse(elements[i + 1].Trim('"', ' '))),
                                        new ParameterOverride("time", time));
                                }
                                catch (Exception)
                                {
                                    
                                }
                                
                                result[dataTypes[i]] = data;
                            }
                            yield return
                                new Tuple<IStockStaticInformation, IDictionary<StockDataType, IStockDynamicData>>(
                                    stockInformations[elements[0].Trim('"').ToUpper()], result);
                            line = sr.ReadLine();
                        }
                    }
                }
            }
            finally
            {
                if (response != null) response.Close();
            }
        }

        /// <summary>
        /// return all available data between the start date and end date
        /// </summary>
        /// <param name="stockInfo"></param>
        /// <param name="startDate"></param>
        /// <param name="endDate"></param>
        /// <returns></returns>
        /// <remarks>
        /// please be noticed the date of the first data is not equal to the startDate, 
        /// because maybe you specified a night time while the stock market opens at day time
        /// </remarks>
        public IEnumerable<IDictionary<StockDataType, IStockDynamicData>> GetHistoryData(IStockStaticInformation stockInfo, DateTime startDate, DateTime endDate)
        {
            if (startDate > endDate)
            {
                yield break;
            }
            if (endDate > DateTime.Now)
            {
                endDate = DateTime.Now;
            }
            string url;
            var currentStartDate = startDate;
            if ((DateTime.Now - startDate).TotalDays <= 1)
            {
                url = "http://chartapi.finance.yahoo.com/instrument/1.0/{0}/chartdata;type=quote;range=1d";
            }
            else if ((DateTime.Now - startDate).TotalDays <= 5)
            {
                url = "http://chartapi.finance.yahoo.com/instrument/1.0/{0}/chartdata;type=quote;range=5d";
            }
            else
            {
                url = "http://chartapi.finance.yahoo.com/instrument/1.0/{0}/chartdata;type=quote;ys={1};yz=0"; //yz is the span between the end year and the start year, but set it to 0 will get the most data
                currentStartDate = new DateTime(startDate.Year, 1, 1);
            }
            
            while (currentStartDate <= endDate)
            {
                var currentUrl = string.Format(url, GetStockQueryId(stockInfo), currentStartDate.Year);
                var request = WebRequest.Create(currentUrl);
                var response = request.GetResponse();
                try
                {
                    using (var stream = response.GetResponseStream())
                    {
                        var doc = XDocument.Load(stream);
                        var valuesMeta = doc.Descendants("values-meta").First();
                        var stockTypeSecquence = new List<StockDataType>();
                        foreach (var val in valuesMeta.Elements())
                        {
                            StockDataType t;
                            switch (val.Attribute("id").Value)
                            {
                                case "close":
                                    t = StockDataType.LastTradePrice;
                                    break;
                                case "high":
                                    t = StockDataType.HighPrice;
                                    break;
                                case "low":
                                    t = StockDataType.LowPrice;
                                    break;
                                case "open":
                                    t = StockDataType.Open;
                                    break;
                                case "volume":
                                    t = StockDataType.Volume;
                                    break;
                                default:
                                    throw new NotImplementedException();
                            }
                            stockTypeSecquence.Add(t);
                        }
                        var timeType = doc.Descendants("reference-meta").First().Descendants("type").First().Value;
                        var timeParser = ResolveValueParser((TimeType)Enum.Parse(typeof(TimeType), timeType));
                        var datas = doc.Descendants("series").Descendants("p");

                        foreach (var data in datas)
                        {
                            var time = (DateTime)timeParser.Parse(data.Attribute("ref").Value);
                            if (time < startDate)
                            {
                                continue;
                            }
                            if (time > endDate)
                            {
                                yield break;
                            }
                            IDictionary<StockDataType, IStockDynamicData> oneData =
                                new Dictionary<StockDataType, IStockDynamicData>();
                            int index = 0;
                            foreach (var stockData in data.Elements())
                            {
                                oneData[stockTypeSecquence[index]] =
                                    ResolveStockDynamicData(stockTypeSecquence[index],
                                                            new ParameterOverride("dataType", stockTypeSecquence[index]),
                                                            new ParameterOverride("value",
                                                                                  ResolveValueParser(
                                                                                      stockTypeSecquence[index]).Parse(
                                                                                          stockData.Value)),
                                                            new ParameterOverride("time", time));
                                index++;
                            }
                            yield return oneData;
                        }
                    }
                }
                finally
                {
                    if (response != null) response.Close();
                }
                currentStartDate = currentStartDate.AddYears(1);
            }
            
        }

        public bool SupportGetCurrentData
        {
            get { return true; }
        }

        public bool SupportGetHistoryData
        {
            get { return true; }
        }
    }
}
